<?php

namespace Core\Data;

const GENERATOR_SIMULATE = 1;
const GENERATOR_WIPE = 2;
const GENERATOR_UPDATE = 4;
const GENERATOR_BACKUP = 8;

class Generator {

    private $UDD;
    private $strReport = "";

    // Members used by generateDatabases().
    private $ConnectionEx;
    private $boolSimulate;
    private $boolWipe;
    private $boolUpdate;
    private $arrIgnoredFieldsNotToList;

    function __construct () {        
        $this->UDD = new UDD\Structure ();
    }

    /**
     * Outputs an HTML document with a report.
     *
     * The report details everything the generator has done so far.
     * 
     * @param string $strHTML
     */
    public function report ( &$strHTML = null ) {

        $objHTML = new \Core\Presentation\HTML\Document ();
        $objHTML->Title = "Core\Data\Generator::report()";

        $objHTML->addStyle( "body", "background-color", "rgb(220,220,220)" );
        $objHTML->addStyle( "h1, h4, p", "font-family", '"Helvetica", "Verdana"' );
        $objHTML->addStyle( "h1", "color", "rgb(48,48,48)" );
        $objHTML->addStyle( "h4", "color", "white" );
        $objHTML->addStyle( "h4", "margin", "0 0 7px 0" );
        $objHTML->addStyle( "p, div", "color", "rgb(119,119,119)" );
        $objHTML->addStyle( "p", "font-size", "10pt" );
        $objHTML->addStyle( "span.Version", "font-size", "8pt" );

        $objHTML->addStyle( "div.Table", "font-size", "10pt" );
        $objHTML->addStyle( "p.Test, p.Documents, p.Wipe, div.Table, p.QueriesSent, p.CreatedTable, p.IgnoredTable, p.SuffixedTable", "margin", "0" );
        $objHTML->addStyle( "p.Test, p.Documents, p.Wipe, div.Table, p.QueriesSent, p.CreatedTable, p.IgnoredTable, p.SuffixedTable", "font-family", '"Courier"' );
        $objHTML->addStyle( "p.Test, p.Documents, p.Wipe, div.Table, p.QueriesSent, p.CreatedTable, p.IgnoredTable, p.SuffixedTable", "text-align", "left" );
        $objHTML->addStyle( "p.Test, p.Documents, p.Wipe, div.Table, p.QueriesSent, p.CreatedTable, p.IgnoredTable, p.SuffixedTable", "padding", ".5em 1em .5em 1em" );
        $objHTML->addStyle( "p.Test, p.Documents, p.QueriesSent", "background-color", "rgb(213,213,213)" );
        $objHTML->addStyle( "p.Wipe, div.Table, p.CreatedTable, p.IgnoredTable, p.SuffixedTable", "background-color", "rgb(203,203,203)" );
        $objHTML->addStyle( "p.Test", "font-weight", "bold" );
        $objHTML->addStyle( "p.Wipe, span.SuffixedField, p.SuffixedTable", "color", "rgb(200,80,170)" );
        $objHTML->addStyle( "span.CreatedField, p.CreatedTable", "color", "rgb(80,80,220)" );
        $objHTML->addStyle( "span.Mismatch, dd.Mismatch", "color", "rgb(210,80,80)" );
        $objHTML->addStyle( "span.IgnoredField, p.IgnoredTable", "color", "rgb(200,150,80)" );
        $objHTML->addStyle( "span.OK", "color", "rgb(90,150,90)" );

        $objHTML->addStyle( "div[id=Report]", "background-color", "rgb(174,174,174)" );
        $objHTML->addStyle( "div[id=Report]", "margin", "2em 0 1em 0" );
        $objHTML->addStyle( "div[id=Report]", "padding", "7px 0 0 0" );
        $objHTML->addStyle( "div[id=Report]", "border", "1px solid rgb(160,160,160)" );

        $objHTML->addStyle( "div[id=Container]", "margin", "0 auto 0 auto" );
        $objHTML->addStyle( "div[id=Container]", "width", "720px" );
        $objHTML->addStyle( "div[id=Container]", "text-align", "center" );


        $strHTML = "";
        $strHTML .= '<div id="Container">';
        $strHTML .= "<h1>Core\Data\Generator::report()</h1>";
        $strHTML .= '<div id="Reports">';
        $strHTML .= "</div>";
        $strHTML .= "<p>" . \Core\NAME . '<br><span class="Version">' . \Core\version() . "</span></p>";
        $strHTML .= "</div>";

        $objHTML->importHTML( $strHTML );

        $objHTML->find( "#Reports" )->get(0)->importHTML( $this->strReport );

        echo $objHTML->HTML;

    }

    public function addDefinition ( $strFilename ) {
        $this->UDD->interpretDocument( $strFilename );
    }

    public function generateModels () {

    }

    /**
     * Generates databases as defined.
     *
     * @param Core_Data_SQL_Connection $Connection
     * @param boolean $boolWipe
     * @exception UnexpectedValueException 0 No databases seems to be defined.
     */
    public function generateDatabases ( SQL\ConnectionEx $ConnectionEx, $constModes = null ) {

        // Other methods this method use need this.
        $this->ConnectionEx = $ConnectionEx;
        

        // Break down $constModes.
        $this->boolSimulate = ( GENERATOR_SIMULATE === ( GENERATOR_SIMULATE & $constModes ) ) ? true : false;
        $this->boolWipe = ( GENERATOR_WIPE === ( GENERATOR_WIPE & $constModes ) ) ? true : false;
        $this->boolUpdate = ( GENERATOR_UPDATE === ( GENERATOR_UPDATE & $constModes ) ) ? true : false;
        $this->boolBackup = ( GENERATOR_BACKUP === ( GENERATOR_BACKUP & $constModes ) ) ? true : false;

        // Start the report printing.
        $this->strReport .= '<div id="Report">';


        // Print to the report that this function has been called.
        $this->strReport .= "<h4>Core\Data\Generator::generateDatabases()</h4>";


        // Print to the report that we're in test mode, if we are.
        $this->strReport .= ( $this->boolSimulate ) ? '<p class="Test">YOU ARE IN SIMULATION MODE. NO ACTUAL CHANGES WILL BE MADE.</p>' : "";


        // Check if any documents has been added and throw an exception if not.
        if ( count( (array) $this->UDD->documents() ) == 0 ) {
            throw new UnexpectedValueException( "No UDD documents seems to be added.", 0);
        }


        // Print to the report wich UDD documents we'll use.
        $this->strReport .= '<p class="Documents">';
        foreach ( $this->UDD->documents() as $objUDDDocument ) {
            $this->strReport .= 'Using "' . $objUDDDocument->getFilename() . '"';
            $this->strReport .= ( $objUDDDocument->getRevision() ) ? " (revision " . $objUDDDocument->getRevision() . ")" : "";
            $this->strReport .= ".<br>";
        }
        $this->strReport .= "</p>";


        // Check if any databases has been defined and throw an exception if not.
        if ( count( (array) $this->UDD->databases() ) == 0 ) {
            throw new UnexpectedValueException( "No databases seems to be defined.", 0);
        }


        // We need to know how many queries $ConnectionEx has already sent.
        $intOriginalNumQueriesSent = $ConnectionEx->getNumQueriesSent();


        // We need to know what database was originally set in $ConnectionEx before we change it.
        $strOriginalDatabase = $ConnectionEx->getCurrentDatabase();


        foreach ( $this->UDD->databases() as $objUDDDatabase ) {

            // NOTE: This all assumes that the databases we intend to use are already there.

            // Set database.
            if ( (string) $objUDDDatabase == "__default" ) {
                $ConnectionEx->setDatabase( $strOriginalDatabase );
            } else {
                $ConnectionEx->setDatabase( $objUDDDatabase );
            }


            // Wipe the database if $constMode = WIPE.
            if ( $this->boolWipe ) {
                if ( !$this->boolSimulate ) { // Ensure that we're not in simulation mode.
                    $ConnectionEx->wipe();
                }
                $this->strReport .= '<p class="Wipe">Database  <b>' . $ConnectionEx->getCurrentDatabase() . "</b> was wiped.</p>";
            }


            // Walk through the defined tables and update or create them.
            foreach ( $objUDDDatabase->tables() as $objUDDTable ) {

                if ( $ConnectionEx->isTable( $objUDDTable ) ) {
                    // The table exists -- update it.

                    $this->strReport .= "<div class=\"Table\">Table <b>$objUDDTable</b> exists. Checking fields...<br>";


                    // Walk through the defined fields and update or create them.
                    foreach ( $objUDDTable->fields() as $objUDDField ) {

                        if ( $ConnectionEx->isField( $objUDDField, $objUDDTable ) ) {
                            // The field exists -- update it.

                            $this->strReport .= "Checking field <b>$objUDDField</b>... ";

                            $objFieldInformation = $ConnectionEx->getFieldInformation( $objUDDField, $objUDDTable );

                            // Used to tell if there was a mismatch between the database and the UDD.
                            $boolMismatch = false;

                            // Check data type.
                            if ( $objFieldInformation->Type != $this->convertUDDDataType( $objUDDField->DataType, $objUDDField->Size ) ) {
                                $this->strReport .= ( !$boolMismatch ) ? '<span class="Mismatch">Not matching:</span>' : "";
                                $this->strReport .= "<dd class=\"Mismatch\">Data type doesn't match. (Database: $objFieldInformation->Type, UDD: " . $this->convertUDDDataType( $objUDDField->DataType, $objUDDField->Size ) . ")</dd>";
                                $boolMismatch = true;
                            }

                            // Check size.
                            if ( $objFieldInformation->Size != $this->convertUDDSize( $objUDDField->DataType, $objUDDField->Size ) ) {
                                $this->strReport .= ( !$boolMismatch ) ? '<span class="Mismatch">Not matching:</span>' : "";
                                $this->strReport .= "<dd class=\"Mismatch\">Size doesn't match. (Database: $objFieldInformation->Size, UDD: $objUDDField->Size)</span></dd>";
                                $boolMismatch = true;
                            }

                            // Check null versus required.
                            if ( $objFieldInformation->Null == $objUDDField->Required ) {
                                $this->strReport .= ( !$boolMismatch ) ? '<span class="Mismatch">Not matching:</span>' : "";
                                $this->strReport .= "<dd class=\"Mismatch\">Null versus required doesn't match. (Database: " . var_export( $objFieldInformation->Null, true ) . ", UDD: " . var_export( $objUDDField->Required, true ) . ")</dd>";
                                $boolMismatch = true;
                            }

                            // Check auto increment.
                            if ( $objFieldInformation->AutoIncrement != $objUDDField->AutoIncrement ) {
                                $this->strReport .= ( !$boolMismatch ) ? '<span class="Mismatch">Not matching:</span>' : "";
                                $this->strReport .= "<dd class=\"Mismatch\">Auto increment doesn't match. (Database: " . var_export( $objFieldInformation->AutoIncrement, true ) . ", UDD: " . var_export( $objUDDField->AutoIncrement, true ) . ")</dd>";
                                $boolMismatch = true;
                            }

                            // Check primary key.
                            if ( ( $objFieldInformation->PrimaryKey and $objUDDField->KeyType != UDD\PRIMARY_KEY ) or ( !$objFieldInformation->PrimaryKey and $objUDDField->KeyType == UDD\PRIMARY_KEY ) ) {
                                $this->strReport .= ( !$boolMismatch ) ? '<span class="Mismatch">Not matching:</span>' : "";
                                $this->strReport .= "<dd class=\"Mismatch\">Primary key doesn't match. (Database: " . var_export( $objFieldInformation->PrimaryKey, true ) . ", UDD: " . var_export( ( $objUDDField->KeyType == UDD\PRIMARY_KEY ) ? true : false, true ) . ")</dd>";
                                $boolMismatch = true;
                            }

                            // A list of fields that we deprecate and do not want to show as ignored.
                            $this->arrIgnoredFieldsNotToList = array();

                            if ( $boolMismatch ) {
                                // There is a mismatch between the database and the UDD -- replace the field.

                                // Suffix the existing field.
                                $this->suffixField( $objUDDField, $objUDDTable, "non matching" );

                                // Add the new field.
                                $this->createFieldFromUDD( $objUDDField ); // NOTE: This function writes to the report for us.

                            } else {
                                // There is no mismatch between the database and the UDD.
                                $this->strReport .= '<span class="OK">Field OK.</span><br>';
                            }

                        } else {
                            // The field does not exist -- create it.
                            $this->createFieldFromUDD( $objUDDField ); // NOTE: This function writes to the report for us.
                        }

                    }


                    // Suffix fields that exists, but are not defined.
                    foreach ( (array) $ConnectionEx->getFields( $objUDDTable ) as $objFieldInformation ) {

                        // Shorten this.
                        $strFieldName = $objFieldInformation->Name;

                        if ( !$objUDDTable->isField( $strFieldName ) and substr( $strFieldName, -11 ) != "-deprecated" ) {
                            // Suffix the field.
                            $this->suffixField( $strFieldName, $objUDDTable, "deprecated" );
                        } elseif ( !$objUDDTable->isField( $objFieldInformation->Name ) ) {
                            // The field is already suffixed -- ignore it.
                            if ( !in_array( $objFieldInformation->Name, $this->arrIgnoredFieldsNotToList ) ) { // Do not list fields that we just deprecated.
                                $this->strReport .= "<span class=\"IgnoredField\">Ignoring field <b>$objFieldInformation->Name</b>.</span><br>";
                            }
                        }

                    }

                    $this->strReport .= "</div>";

                } else {
                    // The table does not exist -- create it.
                    $this->createTableFromUDD( $objUDDTable ); // NOTE: This function writes to the report for us.
                }

            }

            // Suffix tables that exists, but are not defined.
            foreach ( (array) $ConnectionEx->getTables() as $strTableName ) {

                if ( !$objUDDDatabase->isTable( $strTableName ) and substr( $strTableName, -11 ) != "-deprecated" ) {
                    // Suffix the table.

                    // Find a unique suffix.
                    $i = 0;
                    while ( $ConnectionEx->isTable( "$strTableName-$i-deprecated" ) ) {
                        $i++;
                    }
                    $strSuffixedTableName = "$strTableName-$i-deprecated";

                    if ( !$this->boolSimulate ) { // Ensure that we're not in simulation mode.
                        $ConnectionEx->renameTable( $strTableName, $strSuffixedTableName );
                    }
                    $this->strReport .= "<p class=\"SuffixedTable\">Suffixed deprecated table <b>$strTableName</b> to <b>$strSuffixedTableName</b>.</p>";

                } elseif ( !$objUDDDatabase->isTable( $strTableName ) ) {
                    // The table is already suffixed -- ignore it.
                    $this->strReport .= "<p class=\"IgnoredTable\">Ignoring table <b>$strTableName</b>.</p>";
                }

            }

        }


        // Print to the report how many queries was sent
        $intNumQueriesSent = $ConnectionEx->getNumQueriesSent() - $intOriginalNumQueriesSent;
        $this->strReport .= "<p class=\"QueriesSent\">A total of $intNumQueriesSent quer" . ( ( $intNumQueriesSent != 1 ) ? "ies": "y") . " was sent.</p>";


        // End the console and report printing.
        $this->strReport .= "</div></div>";

        // Make sure the database set in $ConnectionEx is the same as it originally was.
        if ( $ConnectionEx->getCurrentDatabase() != $strOriginalDatabase ) {
            $ConnectionEx->setDatabase( $strOriginalDatabase );
        }

    }

    public static function generateDefinitions () {

    }


    // These methods are used by generateDatabases().

    /**
     * Adds a field to a table based on an instance of UDD\Field.
     */
    private function createFieldFromUDD ( $objUDDField ) {

        $objFieldInformation = new SQL\FieldInformation ();
        $objFieldInformation->Name = $objUDDField;
        $objFieldInformation->Type = $this->convertUDDDataType( $objUDDField->DataType, $objUDDField->Size );
        $objFieldInformation->Size = ( $this->convertUDDSize( $objUDDField->DataType, $objUDDField->Size ) ) ?: null;
        $objFieldInformation->Null = ( $objUDDField->Required ) ? false : true;
        $objFieldInformation->AutoIncrement = $objUDDField->AutoIncrement;
        $objFieldInformation->PrimaryKey = ( $objUDDField->KeyType == UDD\PRIMARY_KEY ) ? true : false;

        if ( $objFieldInformation->AutoIncrement ) {
            $strFormerAutoIncrementingField = $this->ConnectionEx->getAutoIncrementingField( $objUDDField->Table );
        }

        if ( $objFieldInformation->PrimaryKey ) {
            $strFormerPrimaryKey = $this->ConnectionEx->getPrimaryKey( $objUDDField->Table );
        }

        if ( !$this->boolSimulate ) { // Ensure that we're not in simulation mode.
            $this->ConnectionEx->createField( $objFieldInformation, $objUDDField->Table );
        }

        // Print a line to the report.
        $this->strReport .= "<span class=\"CreatedField\">Created new field <b>$objUDDField</b>.</span><br>";

        if ( $strFormerAutoIncrementingField ) {
            $this->strReport .= "<span class=\"SuffixedField\"><b>$strFormerAutoIncrementingField</b> is no longer auto incrementing.</span><br>";
        }

        if ( $strFormerPrimaryKey ) {
            $this->strReport .= "<span class=\"SuffixedField\"><b>$strFormerPrimaryKey</b> is no longer primary key.</span><br>";
        }

    }

    private function createTableFromUDD ( $objUDDTable ) {

        $strQuery = "CREATE TABLE `$objUDDTable` ( ";

        foreach ( $objUDDTable->fields() as $objUDDField ) {

            $strQuery .= "`$objUDDField` ";

            // Figure out the query text to add for data type and size.
            switch ( $objUDDField->DataType ) {
                case UDD\INT8:
                case UDD\INT16:
                case UDD\INT24:
                case UDD\INT32:
                case UDD\INT64:
                case UDD\UNIXTIME32:
                case UDD\UNIXTIME64:
                    $strQuery .= $this->convertUDDDataType( $objUDDField->DataType, $objUDDField->Size );
                    break;
                case UDD\TEXT:
                    if ( $objUDDField->Size ) {
                        $strQuery .= $this->convertUDDDataType( $objUDDField->DataType, $objUDDField->Size ) . "($objUDDField->Size)";
                        break;
                    }
                    $strQuery .= $this->convertUDDDataType( $objUDDField->DataType, $objUDDField->Size );
                    break;
                case UDD\BOOLEAN:
                case UDD\EMAIL:
                case UDD\MD5:
                    $strQuery .= $this->convertUDDDataType( $objUDDField->DataType, $objUDDField->Size ) . "(" . $this->convertUDDSize( $objUDDField->DataType, $objUDDField->Size ) . ")";
                    break;
            }

            $strQuery .= ( $objUDDField->Required ) ? " NOT NULL" : " NULL";
            $strQuery .= ( $objUDDField->AutoIncrement ) ? " AUTO_INCREMENT" : "";

            switch ( $objUDDField->KeyType ) {
                case UDD\PRIMARY_KEY:
                    $strQuery .= " PRIMARY KEY";
            }

            $strQuery .= ", ";

        }

        $strQuery = substr( $strQuery, 0, -2 );
        $strQuery .= " )";

        if ( !$this->boolSimulate ) { // Ensure that we're not in simulation mode.
            $this->ConnectionEx->query( $strQuery );
        }
        $this->strReport .= "<p class=\"CreatedTable\">Created new table <b>$objUDDTable</b> with " . count( (array) $objUDDTable->fields() ) . " field" . ( ( count( (array) $objUDDTable->fields() ) == 1 ) ? "" : "s" ) . ".</p>";

        
    }

    private function suffixField ( $strFieldName, $strTableName, $strText ) {

        $boolAutoIncrementing = ( $this->ConnectionEx->getAutoIncrementingField( $strTableName ) == $strFieldName ) ? true : false;
        $boolPrimaryKey = ( $this->ConnectionEx->getPrimaryKey( $strTableName ) == $strFieldName ) ? true : false;

        // Find a unique suffix.
        $i = 0;
        while ( $this->ConnectionEx->isField( "$strFieldName-$i-deprecated", $strTableName ) ) {
            $i++;
        }
        $strSuffixedFieldName = "$strFieldName-$i-deprecated";

        // Do not list this field as ignored later.
        $this->arrIgnoredFieldsNotToList[] = $strSuffixedFieldName;

        if ( !$this->boolSimulate ) { // Ensure that we're not in simulation mode.
            $this->ConnectionEx->renameField( $strFieldName, $strSuffixedFieldName, $strTableName  );
        }
        $this->strReport .= "<span class=\"SuffixedField\">Suffixed $strText field <b>$strFieldName</b> to <b>$strSuffixedFieldName</b>.</span><br>";

        if ( $boolAutoIncrementing ) {
            if ( !$this->boolSimulate ) { // Ensure that we're not in simulation mode.
                $this->ConnectionEx->unsetAutoIncrement( $strTableName );
            }
            $this->strReport .= "<span class=\"SuffixedField\"><b>$strSuffixedFieldName</b> is no longer auto incrementing.</span><br>";
        }

        if ( $boolPrimaryKey ) {
            if ( !$this->boolSimulate ) { // Ensure that we're not in simulation mode.
                $this->ConnectionEx->unsetPrimaryKey( $strTableName );
            }
            $this->strReport .= "<span class=\"SuffixedField\"><b>$strSuffixedFieldName</b> is no longer primary key.</span><br>";
        }

    }

    private function convertUDDSize ( $constUDDDataType, $intSize ) {
        switch ( $constUDDDataType ) {
            case UDD\BOOLEAN:
                return 1;
            case UDD\INT8:
                return 4;
            case UDD\INT16:
                return 6;
            case UDD\INT24:
                return 9;
            case UDD\INT32:
            case UDD\UNIXTIME32:
                return 11;
            case UDD\INT64:
            case UDD\UNIXTIME64:
                return 20;
            case UDD\TEXT:
                if ( $intSize ) {
                    return $intSize;
                }
                return false;
            case UDD\EMAIL:
                return 255;
            case UDD\MD5:
                return 32;
        }
    }

    private function convertUDDDataType ( $constUDDDataType, $intSize ) {
        switch ( $constUDDDataType ) {
            case UDD\BOOLEAN:
            case UDD\INT8:
                return "TINYINT";
            case UDD\INT16:
                return "SMALLINT";
            case UDD\INT24:
                return "MEDIUMINT";
            case UDD\INT32:
            case UDD\UNIXTIME32:
                return "INT";
            case UDD\INT64:
            case UDD\UNIXTIME64:
                return "BIGINT";
            case UDD\TEXT:
                if ( $intSize ) {
                    return "VARCHAR";
                }
                return "TEXT";
            case UDD\EMAIL:
                return "VARCHAR";
            case UDD\MD5:
                return "CHAR";
        }
    }

}

?>
